#
# Copyright (c) 2021, 2023 supercell
#
# SPDX-License-Identifier: BSD-3-Clause
#
module Luce
  alias Resolver = Proc(String, String?, Node?)

  # Base class for any AST item.
  #
  # Roughly corresponds to Node in the DOM. Will be either an
  # Element or Text.
  abstract class Node
    abstract def accept(visitor : NodeVisitor) : Nil

    abstract def text_content : String
  end

  # A named tag that can contain other nodes.
  class Element < Node
    getter tag : String
    getter children : Array(Node)?
    getter attributes : Hash(String, String?)
    property generated_id : String?
    property footnote_label : String?

    # Initializes a *tag* Element with *children*.
    def initialize(@tag : String, @children : Array(Node)?)
      @attributes = Hash(String, String?).new
    end

    # Initializes an empty, self-closing *tag* element.
    def self.empty(tag : String) : Element
      Element.new(tag, nil)
    end

    # Initializes a *tag* Element with no `children`.
    def self.with_tag(tag : String) : Element
      Element.new(tag, Array(Node).new)
    end

    # Initializes a *tag* Element with a single Text child.
    def self.text(tag : String, text : String) : Element
      Element.new(tag, [Text.new(text)] of Node)
    end

    # Whether this element is self-closing.
    def empty? : Bool
      children.nil?
    end

    def accept(visitor : NodeVisitor) : Nil
      if visitor.visit_element_before? self
        unless children.nil?
          # ameba:disable Lint/NotNil
          children.not_nil!.each do |child|
            child.accept visitor
          end
        end
        visitor.visit_element_after self
      end
    end

    def text_content : String
      children = @children
      children.nil? ? "" : children.map(&.text_content).join
    end
  end

  # A plain text element
  class Text < Node
    getter text : String

    def initialize(text : String)
      @text = text
    end

    def accept(visitor : NodeVisitor) : Nil
      visitor.visit_text self
    end

    def text_content : String
      @text
    end
  end

  # Inline content that has not been parsed into inline nodes (strong,
  # links, etc).
  #
  # These placeholder nodes should only remain in place while the block
  # nodes of a document are still being parsed, in order to gather all
  # reference link definitions.
  class UnparsedContent < Node
    getter text_content : String

    def initialize(@text_content : String)
    end

    def accept(visitor : NodeVisitor) : Nil
    end
  end

  # Visitor pattern for the AST.
  #
  # Renderers or other AST transformers should inherit this.
  abstract class NodeVisitor
    # Called when a Text node has been reached
    abstract def visit_text(text : Text) : Nil

    # Called when an Element has been reached, before its children have
    # been visited.
    #
    # A return of `false` means to skip its children
    abstract def visit_element_before?(element : Element) : Bool

    # Called when an Element has been reached, after its children have
    # been visited.
    #
    # This will not be called if `visit_element_before?` returns
    # `false`.
    abstract def visit_element_after(element : Element) : Nil
  end
end
